package com.lyw.i10ReentrantReadWriteLock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 1、什么是乐观锁？
 *    - 总是假设最好的情况，每次获取数据的时候都认为别人不会修改，所以不会上锁；但是在更新的时候会判断一下在
 *    此期间别人有没有去更新这个数据，可以使用版本号机制和 CAS 算法实现。
 *    - 乐观锁适用于 多读 的应用类型，这样可以 提高吞吐量 ，像数据库提供的类似于 write_condition 机制，其
 *    实都是提供的乐观锁。
 *    - 在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS
 *    实现的。
 *
 * 2、什么是悲观锁？
 *    - 总是假设最坏的情况，每次获取数据的时候都认为别人会修改，所以每次在获取数据的时候都会上锁，这样别人想
 *    获取这个数据就会阻塞直到它拿到锁后才可以获取（共享资源每次只给一个线程使用，其它线程阻塞，当前线程用完
 *    后再把资源转让给其它线程）。
 *    - 传统的关系型数据库里边就用到了很多这种锁机制，比如行锁、表锁、读锁、写锁等，都是在做操作之前先上锁。
 *    - Java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
 *
 * 3、乐观锁常见的两种实现方式
 *    乐观锁一般会使用版本号机制或 CAS 算法实现。
 *    - 版本号机制
 *      一般是在数据表中加上一个数据版本号 version 字段，表示数据被修改的次数，当数据被修改时，version 值
 *      会加一。当线程 A 要更新数据值时，在读取数据的同时也会读取 version 值，在提交更新时，若刚才读取到
 *      的 version 值与当前数据库中的 version 值相等时才更新，否则将会 重试 更新操作，直到更新成功。
 *
 *      案例:系统的某个表有一个 version 版本字段和一个 balance 余额字段,假设version = 1，balance = 100
 *      - 这时候 线程 A 想要修改数据，首先读取到的 version = 1，修改金额为 50；
 *      - 此时 线程 B 也想要修改数据，首先读取到 version = 1，修改金额为 20
 *      - 线程 A 完成了修改操作，对 version 字段的值进行了 +1 (此时version = 2) 连同金额也提交至数据库
 *      修改，此时由于提交数据版本大于数据库记录当前版本数据被更新完成。(此时数据库版本为 2 )
 *      - 线程 B 也完成了修改操作，对 version 字段的值进行了 +1 (此时值为2)，但此时比对数据库记录版本时发
 *      现，线程 B 提交的数据版本号为 2 ，数据库记录当前版本也为 2 ，不满足 “ 提交版本必须大于记录当前版
 *      本才能执行更新 “ 的乐观锁策略，
 *      - 因此，线程 B 的提交被驳回。这样就避免了线程 B 用基于 version=1 的旧数据修改的结果覆盖线程 A 的
 *      操作结果的可能。
 *    - CAS算法
 *      即 compare and swap （比较与交换） ，是一种有名的无锁算法。无锁编程，即不使用锁的情况下实现多线程
 *      之间的变量同步，也就是在没有线程被阻塞的情况下实现变量的同步，所以也叫非阻塞同步（Non-blocking
 *      Synchronization）。
 *      CAS 算法 涉及到三个操作数：
 *        ① 需要读写的内存值 V
 *        ② 进行比较的值 A
 *        ③ 拟写入的新值 B
 *      当且仅当 V 的值等于 A 时， CAS 通过原子方式用新值 B 来更新 V 的值，否则不会执行任何操作
 *      （比较和替换是一个原子操作）。一般情况下是一个自旋操作，即不断的重试。
 * 4、乐观锁的缺点
 *    - ABA 问题
 *       ABA 问题是乐观锁的一个常见问题。如果一个变量 V 初次读取的时候是 A 值，并且在准备赋值的时候检查到
 *       它仍然是 A 值，那我们就能说明它的值没有被其他线程修改过了吗？很明显是不能的，因为在这段时间它的值可
 *       能被改为其他值，然后又改回 A，那 CAS 操作就会误认为它从来没有被修改过
 *       JDK 1.5 以后的 AtomicStampedReference 类就提供了解决 ABA 问题的方法，其中的 compareAndSet 方
 *       法就是首先检查当前引用是否等于预期引用，并且当前标志是否等于预期标志，如果全部相等，则以原子方式将该
 *       引用和该标志的值设置为给定的更新值。
 *    - 循环时间长开销大
 *       自旋 CAS （也就是更新不成功就一直循环执行直到成功）如果长时间不成功，会给 CPU 带来非常大的执行开销。
 *       如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升，pause 指令有两个作用，第一它可以延
 *       迟流水线执行指令（de-pipeline ）， 使 CPU 不会消耗过多的执行资源，延迟的时间取决于具体实现的版本，
 *       在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突（memory order
 *       violation ）而引起 CPU 流水线被清空（CPU pipeline flush ），从而提高 CPU 的执行效率
 *    - 只能保证一个共享变量的原子操作
 *       CAS 只对单个共享变量有效，当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始，提供了
 *       AtomicReference 类来保证引用对象之间的原子性，你可以把多个变量放在一个对象里来进行 CAS 操作。
 *       所以我们可以使用锁或者利用 AtomicReference 类 把多个共享变量合并成一个共享变量来操作。
 *
 *
 * 表锁
 *    - 假如我们操作数据库的某一张表的任何一个记录都需要获取到这个表锁
 * 行锁
 *    - 假如我们需要操作数据库的某一行记录的时候，需要先获取到行锁。这时候别人就操作不了这行记录
 * 读锁 又叫共享锁 （其他的线程可以访问共享锁不能加其他的锁）
 * 写锁 又叫独占锁 针对行锁 （排他锁）
 *
 * 读写锁
 *    - 指同一时刻只允许一个线程进行访问，而读写锁在同一时刻可以允许多个读线程访问，在写线程访问的时候其
 *    他的读线程和写线程都会被阻塞。读写锁维护一对锁(读锁和写锁)，通过锁的分离，使得并发性提高。
 *    - 读读共享，读写互斥
 *
 *    写锁可以将降级成读锁，读锁不能升级为写锁
 *   缺点
 *    - 一直读或者一直写，不能进行其他操作
 *    - 读完之后才可以写
 * juc.ReadWriter
 *
 * @author lyw
 * @Date 2023/7/10-下午9:18
 */
class MyCache {
  // volatile 强制每个线程从主存中取数据
  // volatile 被用来修饰会被不同线程访问和修改的变量。
  /*
    1、可见性
      - 是指多个线程访问同一个变量时，其中一个线程修改了该变量的值，其它线程能够立即看到修改的值。在 Java 内存
      模型中，所有的变量都存储在主存中，同时每个线程都拥有自己的工作线程，用于提高访问速度。线程会从主存中拷贝变
      量值到自己的工作内存中，然后在自己的工作线程中操作变量，而不是直接操作主存中的变量，由于每个线程在自己的内
      存中都有一个变量的拷贝，就会造成变量值不一致的问题
    2、原子性
      - volatile 只保证单次读/写操作的原子性，对于多步操作，volatile 不能保证原子性
    3、有序性
      - 在 Java 内存模型中，允许编译器和处理器对指令进行重排序，重排序过程不会影响到单线程程序的执行，但是会
      影响到多线程并发执行的正确性。
      - volatile 关键字可以禁止指令重新排序，可以保证一定的有序性。
      - volatile 修饰的变量的有序性有两层含义
        - 所有在 volatile 修饰的变量写操作之前的写操作，将会对随后该 volatile 修饰的变量读操作之后的语句
        可见。
        - 禁止 JVM 重排序：volatile 修饰的变量的读写指令不能和其前后的任何指令重排序，其前后的指令可能会
        被重排序。
   */
  // 可见性是指多个线程访问同一个变量时，其中一个线程修改了该变量的值，其它线程能够立即看到修改的值。
  // 在 Java 内存模型中，所有的变量都存储在主存中，同时每个线程都拥有自己的工作线程，用于提高访问速度。线程会从主存中拷贝变量值到自己的工作内存中，然后在自己的工作线程中操作变量，而不是直接操作主存中的变量，由于每个线程在自己的内存中都有一个变量的拷贝，就会造成变量值不一致的问题
  private static volatile Map<String, Object> map = new HashMap<>();

  private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  // 多线程放数据
  public void put(String key, Object val) {

    Lock lock = readWriteLock.writeLock();
    lock.lock();
    try {
      System.out.println(Thread.currentThread().getName() + " 正在写操作 " + key);
      TimeUnit.MICROSECONDS.sleep(300);
      map.put(key, val);
      System.out.println(Thread.currentThread().getName() + " 完成了写操作");
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      lock.unlock();
    }

  }

  public Object get(String key) {

    Lock lock = readWriteLock.readLock();
    lock.lock();
    Object o = null;
    try {
      System.out.println(Thread.currentThread().getName() + " 开始取数据 " + key);
      TimeUnit.MICROSECONDS.sleep(300);
      o = map.get(key);
      System.out.println(Thread.currentThread().getName() + " 完成了取操作");
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      lock.unlock();
    }
    return o;
  }
}

public class ReentrantReadWriteLockDemo {
  public static final ReadWriteLock rwLock = new ReentrantReadWriteLock();
  public static void main(String[] args) {
    /*MyCache myCache = new MyCache();
    for (int i = 1; i <= 5; i++) {
      final int num = i;
      new Thread(() -> {
        myCache.put(num + "", num + "");
      }, String.valueOf(i)).start();
    }

    for (int i = 1; i <= 5; i++) {
      final int num = i;
      new Thread(() -> {
        myCache.get(num + "");
      }, String.valueOf(i)).start();
    }*/
    // 写锁降级成读锁
    set("123");
    // 读锁升级为写锁
//    get("456");
  }

  public static void set(String key) {
    rwLock.writeLock().lock();
    try {
      System.out.println("开始写操作");
      // 这里会阻塞，等待读锁结束之后才能开始进行写锁
      rwLock.readLock().lock();
      try {
        System.out.println("开始读操作");
      } finally {
        System.out.println("写操作结束");
        rwLock.writeLock().unlock();
      }
    } finally {
      System.out.println("读操作结束");
      rwLock.readLock().unlock();
    }
  }

  public static void get(String key) {
    rwLock.readLock().lock();
    try {
      System.out.println("开始读操作");
      rwLock.writeLock().lock();
      try {
        System.out.println("开始写操作");
        System.out.println("写操作结束");
      } finally {
        rwLock.writeLock().unlock();
      }
      System.out.println("读操作结束");
    } finally {
      rwLock.readLock().unlock();
    }
  }
}
